//
// Created by EmFi on 2023/5/12.
//

#include "Database.h"

Database::Database(const string& fn) : fn(fn) {
    if (!this->_open()) {
        if (!this->_create()) {
            LogUtil::error(this->fn, "Create failed");
            exit(1);
        }
        LogUtil::debug(this->fn, "Created");
    } else {
        stringstream ss;
        ss << "Opened - " << "block_size: " << this->head.block_size << ", length: " << this->head.length << ", indexed: " << this->head.indexed;
        LogUtil::debug(this->fn, ss.str());
    }
}

Database::~Database() {
    this->_close();
    LogUtil::debug(this->fn, "Closed");
}

bool Database::_open() {
    // 打开文件
    this->conn.open(this->fn, ios::binary | ios::in | ios::out);
    if (!this->conn.is_open()) {
        return false;
    }

    // 读取文件头
    this->conn.read((char*)&this->head, sizeof(Head));
    return true;
}

bool Database::_create() {
    // 打开文件
    this->conn.open(this->fn, ios::binary | ios::out);
    if (!this->conn.is_open()) {
        return false;
    }

    // 写出文件头
    Head newHead;
    this->head = newHead;
    this->conn.write((char*)&this->head, sizeof(Head));
    this->conn.flush();

    // 重新打开文件
    this->_close();
    this->_open();
    return true;
}

void Database::_close() {
    if (this->conn.is_open()) {
        this->conn.close();
    }
}

void Database::_optimize() {
    // 读取所有数据
    Point * arr = this->_select_arr(0, this->head.indexed);

    this->_close();

    this->conn.open(this->fn, ios::binary | ios::out);
    this->conn.write((char*)&head, sizeof(Head));

    int wroteIndex = 0;
    for (int i = 0; i < this->head.indexed; i++) {
        if (!arr[i].isNull()) {
            this->conn.write((char*)&arr[i], sizeof(arr[i]));
            wroteIndex++;
        }
    }
    this->head.length = wroteIndex;
    this->head.indexed = wroteIndex;

    this->conn.seekg(0, ios::beg);
    this->conn.write((char*)&head, sizeof(Head));
    this->conn.flush();

    // 重新打开文件
    this->_close();
    this->_open();
}

void Database::optimize() {
    this->_optimize();
    LogUtil::debug(this->fn, "Optimized");
}

void Database::printInfo() const {
    cout << "block_size: " << this->head.block_size << ", length: " << this->head.length << ", indexed: " << this->head.indexed << endl;
}

long Database::count() {
    return this->head.length;
}

int Database::_insert(Point point) {
    // update data
    this->conn.seekp(sizeof(head) + head.block_size * head.indexed, ios::beg);
    this->conn.write((char*)&point, sizeof(point));
    this->conn.flush();
    // update head
    this->head.length += 1;
    this->head.indexed++;
    this->_write_head();
    return this->head.indexed - 1;
}

int Database::insert(Point point) {
    int index = this->_insert(point);
    LogUtil::operate(this->fn, OperationType::Insert, "1 Object(s)");
    return index;
}

int Database::_insert(list<Point> list) {
    // update data
    this->conn.seekp(sizeof(head) + head.block_size * head.indexed, ios::beg);
    for (auto it : list) {
        this->conn.write((char*)&it, sizeof(it));
    }
    this->conn.flush();
    // update head
    this->head.length += list.size();
    this->head.indexed += list.size();
    this->_write_head();
    return this->head.indexed - list.size();
}

int Database::insert(list<Point> list) {
    int index = this->_insert(list);
    LogUtil::operate(this->fn, OperationType::Insert, to_string(list.size()) + " Object(s)");
    return index;
}

void Database::_write_head() {
    this->conn.seekp(0, ios::beg);
    this->conn.write((char*)&head, sizeof(Head));
    this->conn.flush();
}

Point Database::_select(int index) {
    Point tmp;
    this->conn.seekg(sizeof(head) + head.block_size * index, ios::beg);
    this->conn.read((char*)&tmp, sizeof(Point));
    return tmp;
}

Point Database::select(int index) {
    if (index < 0 || index >= head.indexed) {
        LogUtil::error(this->fn, "Index " + to_string(index) + " out of bounds");
        return nullPoint;
    }

    Point tmp = this->_select(index);

    LogUtil::operate(this->fn, OperationType::Select, to_string(tmp.isNull() ? 0 : 1) + " Object(s)");
    return tmp;
}

Point * Database::_select_arr(int index, int length) {
    if (length <= 0) {
        return nullptr;
    }
    auto *arr = new Point[length];
    this->conn.seekg(sizeof(this->head) + this->head.block_size * index, ios::beg);
    this->conn.read((char*)arr, sizeof(Point) * length);
    return arr;
}

list<Point> Database::_select_all() {
    list<Point> list;

    Point* arr = this->_select_arr(0, this->head.indexed);
    for (int i = 0; i < this->head.indexed; i++) {
        // 过滤 null 记录
        if (arr[i].isNull()) {
            continue;
        }
        list.push_back(arr[i]);
    }

    return list;
}

list<Point> Database::selectAll() {
    list<Point> list = this->_select_all();
    LogUtil::operate(this->fn, OperationType::Select, to_string(list.size()) + " Object(s)");
    return list;
}

Point Database::_remove(int index) {
    Point removed;
    // save removed
    this->conn.seekg(sizeof(this->head) + this->head.block_size * index, ios::beg);
    this->conn.read((char*)&removed, sizeof(removed));
    // fill null
    this->conn.seekp(sizeof(this->head) + this->head.block_size * index, ios::beg);
    this->conn.write((char*)&nullPoint, sizeof(nullPoint));
    this->conn.flush();
    // update head
    this->head.length -= 1;
    this->_write_head();
    return removed;
}

Point Database::remove(int index) {
    if (index < 0 || index >= this->head.indexed) {
        LogUtil::error(this->fn, "Index " + to_string(index) + " out of bounds");
        return nullPoint;
    }
    Point removed = this->_remove(index);
    LogUtil::operate(this->fn, OperationType::Delete, "1 Object(s)");
    return removed;
}

Point Database::_update(int index, Point point) {
    // save before
    Point before;
    this->conn.seekg(sizeof(this->head) + this->head.block_size * index, ios::beg);
    this->conn.read((char*)&before, sizeof(before));
    // write items
    this->conn.seekp(sizeof(this->head) + this->head.block_size * index, ios::beg);
    this->conn.write((char*)&point, sizeof(point));
    this->conn.flush();
    return before;
}

Point Database::update(int index, Point point) {
    if (index < 0 || index >= this->head.indexed) {
        LogUtil::error(this->fn, "Index " + to_string(index) + " out of bounds");
        return nullPoint;
    }
    Point before = this->_update(index, point);
    LogUtil::operate(this->fn, OperationType::Update, "1 Object(s)");
    return before;
}

list<Point> Database::query(double xMin, double xMax, double yMin, double yMax, double zMin, double zMax, double wMin,
                            double wMax) {
    list<Point> ret;
    Point tmp;
    for (int i = 0; i < this->head.indexed; i++) {
        tmp = this->_select(i);
        if (
                !tmp.isNull() &&
                tmp.getX() >= xMin && tmp.getX() <= xMax &&
                tmp.getY() >= yMin && tmp.getY() <= yMax &&
                tmp.getZ() >= zMin && tmp.getZ() <= zMax &&
                tmp.getW() >= wMin && tmp.getW() <= wMax
        ) {
            ret.push_back(tmp);
        }
    }
    LogUtil::query(this->fn, this->head.length, ret.size());
    return ret;
}

void Database::_clear() {
    this->_close();
    this->_create();
}

void Database::clear() {
    this->_clear();
    LogUtil::debug(this->fn, "Cleared");
}
